package jannex.label;

import jannex.internal.Hashing;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public abstract class LabelMap<V> extends AbstractMap<String, V> {

    private static final LabelMap<?> EMPTY = new EmptyLabelMap();

    public static <V> Builder<V> builder() {
        return new Builder<>(4);
    }

    public static <V> Builder<V> builder(int capacity) {
        return new Builder<>(capacity);
    }

    @SuppressWarnings("unchecked")
    public static <V> LabelMap<V> empty() {
        return (LabelMap<V>) EMPTY;
    }

    @SuppressWarnings("unchecked")
    public static <V> LabelMap<V> of(String l, V v) {
        return construct(new String[]{l}, (V[]) new Object[]{v});
    }

    @SuppressWarnings("unchecked")
    public static <V> LabelMap<V> of(String l1, V v1, String l2, V v2) {
        return construct(new String[]{l1, l2}, (V[]) new Object[]{v1, v2});
    }


    public static <V> LabelMap<V> of(String l1, V v1, String l2, V v2, String l3, V v3) {
        String[] labels = new String[]{l1, l2, l3};
        @SuppressWarnings("unchecked")
        V[] values = (V[]) new Object[]{v1, v2, v3};
        return construct(labels, values);
    }

    @SuppressWarnings("unchecked")
    public static <V> LabelMap<V> of(String l1, V v1, String l2, V v2, String l3, V v3, String l4, V v4) {
        String[] labels = new String[]{l1, l2, l3, l4};
        @SuppressWarnings("unchecked")
        V[] values = (V[]) new Object[]{v1, v2, v3, v4};
        return construct(labels, values);
    }

    @SuppressWarnings("unchecked")
    public static <V> LabelMap<V> of(String l1, V v1, String l2, V v2, String l3, V v3, String l4, V v4, String l5, V v5) {
        String[] labels = new String[]{l1, l2, l3, l4, l5};
        @SuppressWarnings("unchecked")
        V[] values = (V[]) new Object[]{v1, v2, v3, v4, v5};
        return construct(labels, values);
    }

    @Override
    public abstract LabelSet keySet();

    @Override
    @Deprecated
    public V remove(Object key) {
        throw new UnsupportedOperationException();
    }

    @Override
    @Deprecated
    public void clear() {
        throw new UnsupportedOperationException();
    }

    @Override
    @Deprecated
    public void putAll(Map<? extends String, ? extends V> m) {
        throw new UnsupportedOperationException();
    }

    @Override
    @Deprecated
    public V put(String label, V value) {
        throw new UnsupportedOperationException();
    }

    abstract LabelMap<V> newFromValues(V[] objects);

    public static class Builder<V> {
        private List<String> labels;
        private List<V> values;

        public Builder(int capacity) {
            labels = new ArrayList<>(capacity);
            values = new ArrayList<>(capacity);
        }

        @SuppressWarnings("unchecked")
        public LabelMap<V> build() {
            return construct(labels.toArray(new String[labels.size()]), (V[]) values.toArray());
        }

        public Builder<V> put(String label, V value) {
            if (label == null) {
                throw new NullPointerException();
            }
            labels.add(label);
            values.add(value);
            return this;
        }

        public Builder<V> putAll(Map<String, ? extends V> map) {
            for (Entry<String, ? extends V> entry : map.entrySet()) {
                put(entry.getKey(), entry.getValue());
            }
            return this;
        }


    }

    @SuppressWarnings("unchecked")
    private static <V> LabelMap<V> construct(String[] labels, V[] values) {
        if (labels.length == 0) {
            return (LabelMap<V>) EMPTY;
        } else if (labels.length == 1) {
            if (labels[0] == null) {
                throw new NullPointerException();
            }
            return new SingletonLabelMap<>(labels[0], values[0]);
        }
        int tableSize = LabelSet.chooseTableSize(labels.length);
        String[] labelsTable = new String[tableSize];
        V[] valuesTable = (V[]) new Object[tableSize];
        ArrayList<String> labelsList = null;
        ArrayList<V> valuesList = null;
        for (int i = 0; i < labels.length; i++) {
            String label = labels[i];
            V value = values[i];
            int hash = label.toLowerCase().hashCode();
            for (int j = Hashing.smear(hash); ; j++) {
                int index = j & (tableSize - 1);
                String oldLabel = labelsTable[index];
                if (oldLabel == null) {
                    if (labelsList != null) {
                        labelsList.add(label);
                        valuesList.add(value);
                    }
                    // Came to an empty slot. Put the label here.
                    labelsTable[index] = label;
                    valuesTable[index] = values[i];
                    break;
                } else if (oldLabel.equalsIgnoreCase(label)) {
                    if (labelsList == null) {
                        // first dup
                        labelsList = new ArrayList<>(labels.length);
                        valuesList = new ArrayList<>(labels.length);
                        for (int k = 0; k < i; k++) {
                            String previousLabel = labels[k];
                            V previousValue = values[k];
                            labelsList.add(previousLabel);
                            valuesList.add(previousValue);
                        }
                    }
                    break;
                }
            }
        }
        String[] labelsArray = labelsList == null
                ? labels
                : labelsList.toArray(new String[labelsList.size()]);
        V[] valuesArray = valuesList == null
                ? values
                : valuesList.toArray((V[]) new Object[valuesList.size()]);
        if (labelsArray.length == 1) {
            String label = labelsArray[0];
            V value = valuesArray[0];
            return new SingletonLabelMap<>(label, value);
        } else if (tableSize != LabelSet.chooseTableSize(labelsArray.length)) {
            // Resize the table when the array includes too many duplicates.
            // when this happens, we have already made a copy
            return construct(labelsArray, valuesArray);
        } else {
            return new ArrayLabelMap<>(labelsArray, valuesArray, labelsTable, valuesTable);
        }
    }

}
